Skip to content

Fix nested lambda inference memory growth #5206 - #5207

Open
martinfrancois wants to merge 3 commits into
eclipse-jdt:masterfrom
martinfrancois:agent/issue-5206-nested-lambda-inference
Open

Fix nested lambda inference memory growth #5206#5207
martinfrancois wants to merge 3 commits into
eclipse-jdt:masterfrom
martinfrancois:agent/issue-5206-nested-lambda-inference

Conversation

@martinfrancois

@martinfrancois martinfrancois commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What it does

Fixes #5206.

During speculative overload resolution, ECJ tries each overload candidate without committing to one. It reparses the outer lambda to create a separate copy for that candidate. Each source lambda stores inference results for the target functional-interface types against which ECJ has already tested it. The lambda from which a copy was made is its original lambda.

The outer speculative copy kept that source link, but nested lambdas inside the copy were treated as new originals. They therefore did not reuse the existing per-target results from their original lambdas. Repeating inference for every candidate at every nested level caused combinatorial memory growth and exhausted the heap.

The change links nested speculative copies to their original lambdas. On each branch, it shares the per-target inference cache down to and including the first lambda with parameters. A cache entry is a resolved copy of that lambda and owns its parameter bindings. Nested lambdas may use those bindings, so traversal stops below it and their caches stay local to the enclosing copy. No global cache or shared parser state is introduced.

The source lambda and the copy created by parsing the source again are traversed in the same source order. Each traversal records the current lambda, then stops below it when it has parameters. shareInferenceCaches() therefore receives only lambdas allowed by this parameter-binding rule, so no separate eligibility flag is needed.

The independently designed regression suite has three tests. Two generate a generic and a concrete source. Each source contains 24 nested method calls and 23 parameterless lambdas, alternates between route and select, provides five overload candidates for each selector, and ends at a Work<String> field. The two variants exercise the original missing cache reuse with different inference pressure.

The third test compiles and runs code that mixes lambdas with and without parameters. It includes a nested lambda that reads an enclosing parameter and a separate parameterless sibling. There is no public API change.

The linked NestedLambdaGenerics.java and NestedLambdaNoGenerics.java files provide separate reference validation for the reported behavior; they are not copied into the regression suite.

How to test

  • On unpatched JDT commit 792a9156c119d28063904c85726e486ec2fb2206, compiling each linked OpenJDK source separately with -25 and a 1 GiB heap reproduces Nested lambda overload inference exhausts the heap #5206.
  • With the compiler from this PR, the same commands exit 0 and generate six class files each, with no OutOfMemoryError or internal compiler error.
  • Run NestedLambdaInferenceTest across Java 8 through Java 25 compliance levels: 54 tests pass.
  • Run GenericsRegressionTest_1_8 together with NestedLambdaInferenceTest: 5,670 tests pass, including the parameterized-lambda regression testBug496578.
  • Run the full compiler suite (parser/TestAll, regression/TestAll, and eval/TestAll): 263,722 tests pass with no failures, errors, or skipped tests.

Author checklist

@martinfrancois
martinfrancois force-pushed the agent/issue-5206-nested-lambda-inference branch from 871baa6 to 742245e Compare July 17, 2026 17:17
@martinfrancois
martinfrancois marked this pull request as ready for review July 17, 2026 18:33
@martinfrancois
martinfrancois force-pushed the agent/issue-5206-nested-lambda-inference branch from 742245e to 7b1dd77 Compare July 17, 2026 19:04
@martinfrancois martinfrancois changed the title Fix nested lambda inference memory growth Fix nested lambda inference memory growth #5206 Jul 17, 2026
Link nested speculative copies to their canonical source lambdas so parameterless lambda chains reuse existing per-target inference results without sharing parameter bindings or parser state.

Fixes eclipse-jdt#5206

Signed-off-by: François Martin <f.martin@fastmail.com>
@martinfrancois
martinfrancois force-pushed the agent/issue-5206-nested-lambda-inference branch from 7b1dd77 to 97e0045 Compare July 20, 2026 14:34
@iloveeclipse
iloveeclipse requested a review from Copilot July 22, 2026 13:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a heap-exhaustion scenario in ECJ speculative overload resolution involving deeply nested lambdas by ensuring nested speculative lambda copies remain linked to their canonical source lambdas and can reuse per-target inference caches when safe (no parameters in the enclosing lambda chain). It also adds a focused regression suite to prevent reintroducing the combinatorial memory growth.

Changes:

  • Update LambdaExpression.copy() to align source and copied nested lambdas, linking eligible nested copies to canonical source lambdas and sharing copiesPerTargetType caches when the enclosing lambda chain is parameterless.
  • Add a new regression test (NestedLambdaInferenceTest) that generates deep nested overload/lambda chains (generic + concrete variants).
  • Register the new regression test in the compiler regression test suite.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java Adds the new regression test class to the standard test suite.
org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/NestedLambdaInferenceTest.java New regression coverage for nested speculative overload resolution without heap blowups.
org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java Links nested speculative lambda copies to canonical sources and shares inference caches where safe.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@srikanth-sankaran

Copy link
Copy Markdown
Contributor

@stephan-herrmann - given this involves inference, I am assuming you will want to be the first reviewer, I have also signed up to review FYI

@stephan-herrmann stephan-herrmann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions after a first scan of changes:

  • could we have some tests that demonstrate the interleaving of lambdas with and without parameters? There is some complexity in this, which I'd like see live in action.
  • would it make sense to add a shortcut such that "normal" scenarios could continue to work the "old" way?

Replace the traversal-index root check with an identity check, document context-local cache ownership below parameterized lambdas, and cover interleaved parameterized and parameterless lambdas.

Signed-off-by: François Martin <f.martin@fastmail.com>

@stephan-herrmann stephan-herrmann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm starting to understand the strategy 😄

I suggest to start the next round by looking at my comment in method collectLambdas(). If my idea there is sound, then some of the other comments might become obsolete.

@martinfrancois

Copy link
Copy Markdown
Contributor Author

Thanks, your pruning idea works and made the code much simpler.

collectLambdas() now records the current lambda and stops before visiting its children when that lambda has parameters. This removes CollectedLambda, the depth counter, endVisit(), and the skip branch in shareInferenceCaches(). shareInferenceCaches() now also creates copiesPerTargetType in one place.

I added the details in the inline replies.

After this change, all 54 NestedLambdaInferenceTest tests passed. The full compiler suite (parser/TestAll, regression/TestAll, and eval/TestAll) also passed 263,722 tests, with no failures, errors, or skipped tests. The two linked OpenJDK sources also compiled successfully with the same 1 GiB commands used to reproduce #5206. Each command exited 0 and generated six class files.

I also updated the pull request description to match the new approach.

Collect a lambda with parameters and then stop below it. Nested lambdas
may use that lambda's parameter bindings, so their caches must stay local
to the enclosing copy. The lambda itself can share its cache because each
cached copy has its own parameter bindings.

Remove the cache eligibility record and depth counter. Let
shareInferenceCaches() create copiesPerTargetType in one place.

Signed-off-by: François Martin <f.martin@fastmail.com>
@martinfrancois
martinfrancois force-pushed the agent/issue-5206-nested-lambda-inference branch from dd2de41 to b25d5f0 Compare August 6, 2026 19:30
@stephan-herrmann

Copy link
Copy Markdown
Contributor

Thanks, your pruning idea works and made the code much simpler.

Thanks for making these changes. The PR now looks good to me.

@srikanth-sankaran do you want review this, too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested lambda overload inference exhausts the heap

4 participants